#!/usr/bin/python

from optparse import OptionParser

import getpass
import irclib
import random
import re
import shlex
import subprocess
import sys
import textwrap
import threading

#Global constants
version = "0.1"
nickservnick = "NickServ" #Nick of nickserv

#Global variables
verbose = False

#Thread communication variables
all_running = None #Set/True until everybody should die

def main():
    global version
    #Handle command line arguments
    #command: OpenTTD Command line, if not openttd -D
    #channel: IRC channel to which to send output
    #server: IRC server
    #port: IRC server port
    usage = """%prog option [nick-list]

nick-list:    A comma-seperated list of nicks allowed to give commands to the
              OpenTTD server.  If this list is not specified, %prog will
              try to use the username of the user who launched %prog.
              Failing that, all IRC users will be able to
              send commands to OpenTTD.
Example:
%prog friend,mom,uncle
This would connect to the default channel on the default server with the
default nick ottd-rcon.  Only friend, mom, and uncle will be able to send
commands to OpenTTD.
"""
    parser = OptionParser(usage=usage, version=version)
    parser.add_option("-c", "--command", dest="command",
                      default="openttd -D -d net=0", metavar="COMMAND",
                      help="command used to start OpenTTD server [default: %default]")
    parser.add_option("-s", "--server", dest="server", default="irc.oftc.net",
                      help="irc server [default: %default]")
    parser.add_option("-p", "--port", dest="port", default=6667, type="int",
                      help="irc server port [default: %default]"),
    parser.add_option("-a", "--channels", "--chans", dest="channels",
                      default="#ottd-rcon",
                      help="comma separated list of irc channels to join [default: %default]")
    parser.add_option("-n", "--nick", dest="nick", default="ottd-rcon",
                      help="OpenTTD server's nick [default: %default]")
    parser.add_option("-m", "--nickserv", dest="nickserv",
                      metavar="MESSAGE", default="",
                      help="sends MESSAGE to NickServ after connecting to IRC")
    parser.add_option("-@", "--chatter", dest="chatter",
                      action="store_true", default=False,
                      help="Send text in the channel as in-game messages.  " + \
                           "OTTD commands will need to be prefixed with a @.")
    parser.add_option("-Q", "--noquery", dest="noquery", action="store_true",
                      default=True, help="dsable \"dbg: [net] [udp] " +\
                      "queried from\" messages")
    parser.add_option("-v", "--verbose", dest="verbose",  action="store_true",
                      default=True, help="enable verbose output [default]")
    parser.add_option("-q", "--quiet", dest="verbose",  action="store_false",
                      default=True, help="disable verbose output")
#    parser.add_option("-o", "--other", dest="isother", action="store_true",
#                      defualt=False, help="TODO: Finish This
    (options, args) = parser.parse_args()

    #If we've a list of allowed nicks,
    if len(args) > 0:
        #Split it into a list and remove empty users
        users = args[0].split(",")
        while "" in users:
            users.remove("")
    #If not, try the username of the person who launched the process
    else:
        try:
            users = [getpass.getuser()]
        except:
            #If that doesn't work, all users are authorized
            users = []

    #Split up a list of channels
    channels = options.channels.split(",")
    while "" in channels: channels.remove("")

    #Event for thread communication
    global all_running
    all_running = threading.Event()
    all_running.set()

    #Verbosity flag
    global verbose
    verbose = options.verbose

    #Launch the OpenTTD server
    if verbose: print "Launching OpenTTD server [%s]"%options.command
    try:
        openttd = subprocess.Popen(shlex.split(options.command),
                                   stdin=subprocess.PIPE,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.STDOUT)
    except Exception, e: #If it failed
        print "Unable to launch OpenTTD server."
        print "Did you specify the right command with --command?"
        print e
        return


    #Make an IRC object
    ircclient = IRCClient(options.server, options.port, options.nick,
                          channels, users=users, tottd=openttd.stdin,
                          nickservmsg=options.nickserv, chatter=options.chatter,
                          noquery=options.noquery)
    #Make Threads to handle OTTD output and stdin input
    ottdout = OTTDOut(openttd, ircclient)
    ottdin = OTTDIn(openttd)

    #Start them running
    ottdout.start()
    ottdin.start()
    ircclient.start()

    #Wait for them to finish
    openttd.wait()
    all_running.clear()
    ottdout.join()
    #Yeah, it's a hack.  You have a better idea?
    sys.exit()
    ottdin.join()

    return

#Thread to handle output from OpenTTD
#Constructor Arguments:
#openttd: OpenTTD's Popen object
#ircclient: The IRC client, with a method toirc(msg) that sends msg.
class OTTDOut(threading.Thread):
    openttd = None #OTTD Output
    irc = None #IRC Client
    def __init__(self, openttd, ircclient):
        threading.Thread.__init__(self)
        self.openttd = openttd
        self.irc = ircclient
        self.daemon = True
        return

    #Main method
    def run(self):
        global all_running
        #Thread name
        self.name = "OTTDOut"
        #Hopefully it's blocking...
        while all_running:
            #Check and see if the process is dead
            if self.openttd.poll() != None:
                #It is.
                all_running.clear()
                return
            #Get a line from OpenTTD
            line = self.openttd.stdout.readline()
            #Send it to IRC and the console
#            sys.stdout.write(line)
#            sys.stdout.flush()
            self.irc.toirc(line)
        return

#Thread to pass stdin to OpenTTD
#Constructor takes OpenTTD's Popen object
class OTTDIn(threading.Thread):
    openttd = None
    def __init__(self, openttd):
        threading.Thread.__init__(self)
        self.openttd = openttd
        self.daemon = True
        return

    #Main method
    def run(self):
        global all_running
        #Thread name
        self.name = "OTTDIn"
        #Hopefully it's blocking...
        while all_running:
            #Check and see if the process is dead
            if self.openttd.poll() != None:
                #It is.
                all_running.clear()
                return
            try:
                #Get a line from stdin
                line = sys.stdin.readline()
                #Send it to OpenTTD
                self.openttd.stdin.write(line)
                self.openttd.stdin.flush()
            #If something's gone wrong, probably time to die
            except:
                all_running.clear()
        return




#TODO: Write code to change target output channel/whatever

#IRC client class
#Constructor needs the following
#Server: The server to which to connect
#Port: The port on the server to which to connect
#Nick: Our client's nick
#Channels: List of channels on the server to join
#Users: Optional list of users allowed to send commands
#tottd: Pipe used to send data to OpenTTD
#nickservmsg: Message to send to NickServe upon connect
#chatter: Boolean expressing whether we should send channel messages as chatter
class IRCClient(irclib.SimpleIRCClient):
    nick = "" #Desired nick for the client
    channels = [] #The channels we're on
    tottd = None #Pipe to send data to OpenTTD
    users = [] #A list of nicks allowed to send commands
    nickservmsg = "" #Message to send to NickServ
    server = ""#IRC server
    port = 0 #Port on IRC server
    verbose = False #Send verbose output
    chatter = False #Send IRC chatter as messages
    noquery = False #Hide messages about being queried

    #Constructor function
    def __init__(self, server, port, nick, channels, users=[],
                 tottd=sys.stdout, nickservmsg="", chatter=False,
                 noquery=False):
        global verbose
        irclib.SimpleIRCClient.__init__(self)
        self.tottd = tottd
        self.server = server
        self.port = port
        self.nick = nick
        self.channels = channels
        self.connect(server, port, self.nick)
        self.verbose = verbose #Verbosity flag
        self.joinchannels()
        self.users = users
        self.chatter = chatter
        self.noquery=noquery
        #Tell the console if we've a list of authorized users
        if len(self.users) > 0: self.__whoauth_console(" ".join(users))
        #Tell nickserv that we're here
        self.nickservmsg = nickservmsg
        if self.nickservmsg: self.tellnickserv()

    #Joins the client's list of channels
    def joinchannels(self):
        for c in self.channels:
            self.connection.join(c)
            if self.verbose:
                print "Joined %s on %s:%i as %s."%(c, self.server,
                                                   self.port, self.nick)
        return


    def tellnickserv(self):
        global nickservnick
        if self.nickservmsg != "":
            self.connection.privmsg(nickservnick, self.nickservmsg)
            if self.verbose:
                msgstr = "Sent message to %s."%nickservnick
                print msgstr
                self.toirc(msgstr)
        return

    #Event handlers

    #In case our nick is in use
    def on_nicknameinuse(self, conn, event):
        #New nick
        self.nick = self.nick + str(random.randint(0,9))
        #Send the new nick to the server
        conn.nick(self.nick)
        for c in self.channels: conn.join(c)
        #Tell the user
        if self.verbose:
            print "Nick in use.  Nick now %s"%self.nick
            print "Please consider specifying a different nick."
        if self.nickservmsg: self.tellnickserv()
        return


    #Handle inbound messages
    def on_pubmsg(self, conn, event):
        #If our nick is said in a channel, get the command and arguments
        if  event.target().startswith("#") and \
            event.arguments()[0].startswith(self.nick):
            #Tokenize message
            message = event.arguments()[0].split()
            #We need more than our nick
            if len(message) < 2: return
            #Extract the command and arguments
            cmd = message[1]
            if len(message) > 2:
                args = message[2:]
            else:
                args = []
            #Output should go to the channel
            output = event.target()
        #If it's sent straight to us, get the command and arguments
        elif event.target() == self.nick:
            #Tokenize message
            message = event.arguments()[0].split()
            #Extract the command and arguments
            cmd = message[0]
            if len(message) > 1:
                args = message[1:]
            else:
                args = []
            #Output should go to the sender
            output = event.source()
        #If we're capturing channel chatter, send it to OpenTTD
        elif self.chatter:
            self.sendaschatter(event)
            return
        #If the command was not sent to us, return
        else:
            return
        #Now that we have a command and its arguments, call the parser
        self.parse_cmd(conn, event, cmd, args, output)
        return
    def on_privmsg(self, conn, event):
        self.on_pubmsg(conn,event)
        return

    #If we're disconnected, reconnect
    def on_disconnect(self, conn, event):
        global all_running
        #Don't care if we're about to die
        if not all_running.is_set():
            return
        #Make sure we're actually disconnected
        if self.connection.is_connected():
            print "Not disconnected.  This is a bug.  Please file a bug report."
            return
        #Reconnect to server
        self.connect(self.server, self.port, self.nick)
        self.joinchannels()
        self.tellnickserv()
        return

    #Command parser
    #Conn and event are the same conn and event for all event handlers
    #Cmd is the command sent by the user
    #Args is a list of arguments to the command
    #Output is the nick or channel to which to send output
    def parse_cmd(self, conn, event, cmd, args, output):
        #Make sure the sender is allowed to send commands
        if not (irclib.nm_to_n(event.source()) in self.users) > 0:
            return
        if self.verbose:
            print ""
            print "FRM: %s"%irclib.nm_to_n(event.source())
            print "CMD: %s"%cmd
            if len(args) > 0:
                print "ARG: %s"%str(args)
            print "OUT: %s"%str(output)
        #Command dictionary
        cmds = {
            "!die": self.die, #Exit openttd
            "!auth": self.auth, #Authorize a user
            "!deauth": self.deauth, #Deauthorize a user
            "!whoauth": self.whoauth, #List authorized users
            "!quiet": self.quiet, #Don't send output to IRC
            "!talk": self.talk, #Send output to IRC
            "!join": self.join, #Join an IRC channel
            "!showchatter": self.showchatter, #Send traffic as messages
            "!hidechatter": self.hidechatter, #Don't send traffic as messages
            "!showqueries": self.showquery,#Show people querying the server
            "!hidequeries": self.hidequery, #Don't show people querying the server
            "!part": self.part, #Part an IRC channel
            "!cycle": self.cycle, #Cycle an IRC Chanel
        }
        #If the command is for the IRC client, use it
        if cmd in cmds.keys():
            cmds[cmd](args, conn, output, event)
        #If not, send it to OpenTTD, either as a command or chatter
        else:
            #If we have a non-command and chatter is on
            if self.chatter and (not cmd.startswith("@")):
                #Send it as chatter
                self.sendaschatter(event)
                return
            #If we have a command and chatter is on
            elif self.chatter and cmd.startswith("@"):
                #Reformat it
                cmd = cmd[1:]
            #Execute the command
            self.ttdcmd([cmd] + args, conn, output)
        return


    #Kill off OpenTTD, the Client, and this program.  Sad day...
    def __die(self):
        #Close the server connection
        if self.connection.is_connected():
            for c in self.channels:
                self.connection.part([c], message="pyottdirc")
            self.connection.disconnect(message="pyottdirc")
            self.connection.close()
        return
    def die(self, args, conn, output, event):
        global all_running
        #Tell everybody else
        all_running.clear()
        #Kill OpenTTD
        self.ttdcmd(["exit"], conn, output)
        #We can stop running now
        self.__die()
        return

    #Allow a user to send/receive commands
    def auth(self, args, conn, output, event):
        #Handle multiple users
        for user in args:
            #Tell us what's happening
            if self.verbose: print "Authorizing user %s."%user
            #Authorize the user
            if user not in self.users:
                self.users.append(user)
            #Tell IRC
            self.__toirc(output, "Authorized %s."%user)
        return

    #De-Authorize a user
    def deauth(self, args, conn, output, event):
        #Handle multiple users
        for user in args:
            #Tell us what's happening
            if self.verbose: print "Deauthorizing user %s."%user
            #Deauthorize the user
            if user in self.users:
                self.users.remove(user)
            #Tell IRC
            self.__toirc(output, "Deauthorized %s."%user)
        return

    #Lists authorized users
    def __whoauth_console(self, userlist):
        #Get a list of 70-char strings of authorized users
        authed = textwrap.wrap(userlist)
        #Tell the console
        print "Authorized users:"
        for line in authed:
            print "     %s"%line
        return
    def whoauth(self, args, conn, output, event):
        #A string of authed users
        userlist = " ".join(self.users)
        #Tell the console
        self.__whoauth_console(userlist)
        #Tell IRC
        self.__toirc(output, "Authorized users: %s"%userlist)
        return

    #Silence the client's in-channel chatter
    def quiet(self, args, conn, output, event):
        global verbose
        verbose = False
        self.verbose = verbose
        return

    #Allow the client to talk again
    def talk(self, args, conn, output, event):
        global verbose
        verbose = True
        self.verbose = verbose
        return

    #Join an IRC channel(s)
    def join(self, args, conn, output, event):
        #Loop through channels
        for arg in args:
            #Tell the console
            if self.verbose: print "Joining %s."%arg
            #Join the channel
            conn.join(arg)
            self.channels.append(arg)
            #Tell IRC
            if self.verbose: self.__toirc(output, "Joined %s."%arg)
        return

    #Part an IRC channel(s)
    def part(self, args, conn, output, event):
        #Make sure we're in the channel
	if args[0] not in self.channels: return
        #Loop through channels
        for arg in args:
            #Tell the console
            if self.verbose: print "Parting %s."%args[0]
            #Part the channel
            conn.part(args[0])
            self.channels.remove(args[0])
            #Tell IRC
            if self.verbose: self.__toirc(output, "Parted %s."%args[0])
        return

    #Cycle an IRC channel.  Suggestion and some code by PeterT
    def cycle(self, args, conn, output, event):
        #If the user specified which channel(s) to cycle, cycle them
        if len(args) > 0:
            #If the user wants to cycle all channels, do that
            if args[0] == "all":
                tocycle = self.channels
            #Otherwise assume a list of channels
            else:
                tocycle = args
        #If the user didn't specify anything, cycle the current channel
        else:
            tocycle = [event.target()]
        #Loop through channels to cycle
        for c in tocycle:
            #Make sure it's a channel
            if not c.startswith("#"): continue

            #Tell the console
            if self.verbose: print "Cycling %s" % c
            #Part the channel
            conn.part(c, "Cycling (%s)."%event.source)
            #Re-join channel
            conn.join(c)
            if self.verbose: self.__toirc(output, "Cycled %s on orders of %s."%
                                          (c, irclib.nm_to_n(event.source())))
        return

    #Show chatter
    def showchatter(self, args, conn, output, event):
        #Tell the console
        if self.verbose: print "Showing chatter."
        #Change the variable
        self.chatter = True
        #Tell IRC
        if self.verbose: self.__toirc(output, "Will echo chatter in-game.")
        return

    #Hide chatter
    def hidechatter(self, args, conn, output, event):
        #Tell the console
        if self.verbose: print "Hiding chatter."
        #Change the variable
        self.chatter = False
        #Tell IRC
        if self.verbose: self.__toirc(output, "Will not echo chatter in-game.")
        return

    #Show query messages
    def showquery(self, args, conn, output, event):
        #Tell the console
        if self.verbose: print "Will send queries to IRC."
        #Change the variable
        self.noquery = False
        #Tell IRC
        if self.verbose: self.__toirc(output, "Will echo queries.")
        return

    #Hide query messages
    def hidequery(self, args, conn, output, event):
        #Tell the console
        if self.verbose: print "Will not send queries to IRC."
        #Change the variable
        self.noquery = True
        #Tell IRC
        if self.verbose: self.__toirc(output, "Will not echo queries.")
        return


    #Send a command to OpenTTD
    def ttdcmd(self, args, conn, output, tellirc=True):
        cmd = " ".join(args)
        #Tell the console
        if self.verbose: print "Sending %s to OpenTTD."%cmd
        #Send the command to OpenTTD
        self.tottd.write(cmd + "\n")
        #Tell IRC
        if self.verbose and tellirc and output:
            self.__toirc(output, "Sent \"%s\" to OpenTTD."%cmd)
        return

    #Sends msg to the joined IRC channel or specified.
    #This will be used for OpenTTD output
    def __toirc(self, target, msg):
        if self.connection.is_connected() and self.verbose:
            self.connection.privmsg(target, msg)
            return
    def toirc(self, msg):
        #Log it to stdout
        if self.verbose: print msg.rstrip().decode('utf-8')
        #If the message is the result of chatter being send, skip it
        if self.chatter:
            #Test for our message pattern
            if re.search(r"\[.*].*\<\S+ \[\S+\]\> ", msg):
                #Ignore it if found
                return
        #If the message is a query, maybe skip it
        if self.noquery:
            #Test for a query
            if msg.startswith("dbg: [net] [udp] queried from '"):
                #Ignore if it's found
                return
        #Send it to IRC
        for c in self.channels: self.__toirc(c, msg)
    #Send some text as chatter
    def sendaschatter(self, event):
        #Command: say "nick [#chan]: message"
        self.ttdcmd([u"say \"<%s [%s]> %s\""%(irclib.nm_to_n(event.source()),
                                           event.target(),
                                           event.arguments()[0])],
                    self.connection, None, tellirc=False)

    #Starts the IRC client
    def start(self, timeout=0.2):
        global all_running
        #Loop until someone says it's time to finish
        while all_running.is_set():
            self.ircobj.process_once(timeout)
        #Just in case we've not done this yet
        try:
            self.__die()
        except:
            pass
        return



if __name__ == "__main__":
    main()



